/**
 * Copyright (c) 2012-2014, Yang Yang <cntototo@gmail.com>
 *
 * This software may be distributed under the terms of the New BSD License.
 * See README for more details.
 */

#include "sta.h"

#define LOG_TAG "STA"

#include "include.h"
#include "global.h"
#include "def.h"
#include "util.h"
#include "log.h"
#include "driver.h"
#include "os.h"
#include "loop.h"

#include "sta_def.h"

#define OK_SNR 35
#define IS_5GHZ(freq) (freq > 4000)

/***********************************************************
 *  Forward declarations
 ***********************************************************
 */
void sta_assoc_timer(void *ctx);
void sta_scan_timer(void *ctx);
enum wfs_internal_err sta_cancel_scan(struct wfs_iface *cur_if);

/***********************************************************
 *  Local functions
 ***********************************************************
 */
static int is_associate_ongoing(const struct wfs_iface *cur_if)
{
	enum wfs_sta_status_associate assoc;

	if (!cur_if || !cur_if->sta_info)
		return FALSE;

	assoc = cur_if->sta_info->status.associate;

	if (assoc == STA_ASSOC_PENDING || assoc == STA_ASSOC_RUNNING)
		return TRUE;

	return FALSE;
}


static enum wfs_internal_err
sta_assoc(struct wfs_iface *cur_if)
{
	enum wfs_internal_err ret = WFS_INTERNAL_ERR_SUCCESS;
	struct wfs_driver_assoc_params *params = NULL;

	assert(cur_if && cur_if->sta_info);

	ENTER();

	if (!cur_if->sta_info->assoc_params) {
		ret = WFS_INTERNAL_ERR_INVAL;
		goto out;
	}
	params = cur_if->sta_info->assoc_params;

	if (is_associate_ongoing(cur_if)) {
		wfs_printf(LOG_INFO, "Reject associate request since one "
				"already exists");
		goto out;
	}

	if (wfs_driver_is_scan_ongoing(cur_if)) {
		wfs_printf(LOG_INFO, "Will start associate after scan is done");
		cur_if->sta_info->status.associate = STA_ASSOC_PENDING;
		goto out;
	}

	cur_if->sta_info->status.associate = STA_ASSOC_RUNNING;

	// TODO: add params validatation

	/**
	 * Before associate, we need:
	 * 1. Cancel scan request and stop scheduled scan
	 */
	ret = sta_cancel_scan(cur_if);
	if (ret)
		goto out;

	ret = wfs_driver_associate(cur_if, params);

	/**
	 * After associate, we need:
	 * 1. If associate failure, re-try
	 * 2. If associate success, EAPOL 4-way handshake
	 */

out:
	if (ret)
		wfs_printf(LOG_ERR, "Failed to initiate STA association.");
	LEAVE();
	return ret;
}


static enum wfs_internal_err
sta_scan(struct wfs_iface *cur_if)
{
	enum wfs_internal_err ret = WFS_INTERNAL_ERR_SUCCESS;
	struct wfs_driver_scan_params *params = NULL;

	assert(cur_if && cur_if->sta_info);

	ENTER();

	if (!cur_if->sta_info->scan_params) {
		ret = WFS_INTERNAL_ERR_INVAL;
		goto out;
	}
	params = cur_if->sta_info->scan_params;

	if (is_associate_ongoing(cur_if)) {
		wfs_printf(LOG_INFO, "Reject scan request since associate is "
				"ongoing");
		goto out;
	}

	if (wfs_driver_is_scan_ongoing(cur_if)) {
		wfs_printf(LOG_INFO, "Scan is already ongoing, "
				"no need to trigger a new one.");
		goto out;
	}

	ret = wfs_driver_scan(cur_if, params);

out:
	if (ret)
		wfs_printf(LOG_ERR, "Failed to initiate STA scan.");
	LEAVE();
	return ret;
}


static int sta_scan_result_compar(const void *a, const void *b)
{
	const struct wfs_driver_scan_result *res_a =
			*(const struct wfs_driver_scan_result **) a;
	const struct wfs_driver_scan_result *res_b =
			*(const struct wfs_driver_scan_result **) b;
	s32 snr_a = 0, snr_b = 0;

	snr_a = res_a->signal_level;
	snr_b = res_b->signal_level;
	if ((res_a->flags & res_b->flags & WFS_SCAN_LEVEL_DBM)
		&& !((res_a->flags | res_b->flags) & WFS_SCAN_NOISE_INVALID)) {
		snr_a = MIN(snr_a, OK_SNR);
		snr_b = MIN(snr_b, OK_SNR);
	}

	/** Better max rate is preferred if SNR is close */
	if ((snr_a && snr_b && abs(snr_a - snr_b) < 5)
		|| (res_a->signal_qual && res_b->signal_qual
			&& abs(res_a->signal_qual - res_b->signal_qual) < 10)) {
		u32 mr_a = get_ie_max_rate(res_a->ie, res_a->ie_len);
		u32 mr_b = get_ie_max_rate(res_b->ie, res_b->ie_len);
		if (mr_a != mr_b)
			return mr_b - mr_a;
		/** 5 Ghz is preferred if max rate is equal */
		if (IS_5GHZ(res_a->freq) ^ IS_5GHZ(res_b->freq))
			return IS_5GHZ(res_a->freq) ? -1 : 1;
	}

	/** Better quality is preferred if SNR is equal */
	if (snr_a == snr_b)
		return res_b->signal_qual - res_a->signal_qual;

	/** Better SNR is preferred */
	return snr_b - snr_a;
}


static void print_scan_results(struct wfs_driver_scan_results *results)
{
	u32 i = 0;

	assert(results);

	if (results->num == 0)
		return;

	wfs_printf(LOG_DEBUG, "Sorted scan results");

	wfs_print_line(LOG_DEBUG, '-');
	for (i = 0; i < results->num; ++i) {
		struct wfs_driver_scan_result *r = results->table[i];
		wfs_printf(LOG_DEBUG, MACSTR " freq=%u signal_qual=%u "
			"signal_level=%u noise_level=%u flags=0x%x ago=%u ms",
			MAC2STR(r->bssid), r->freq, r->signal_qual,
			r->signal_level, r->noise_level, r->flags, r->ago);
		if (r->ie_len)
			wfs_hexdump(LOG_DUMP, "IE", r->ie, r->ie_len);
		if (r->beacon_ie_len)
			wfs_hexdump(LOG_DUMP, "Beacon IE", r->beacon_ie,
					r->beacon_ie_len);
		wfs_print_line(LOG_DEBUG, '-');
	}
}


static void validate_scan_results(struct wfs_iface *cur_if)
{
	struct wfs_sta_info *sta_info = NULL;
	struct wfs_driver_scan_results *results = NULL;
	enum wfs_internal_err ret = WFS_INTERNAL_ERR_SUCCESS;
	u32 i = 0;

	assert(cur_if && cur_if->sta_info && cur_if->sta_info->scan_results);

	sta_info = cur_if->sta_info;
	results = sta_info->scan_results;

	for (i = 0; i < results->num; ++i) {
		struct wfs_driver_scan_result *res = results->table[i];
		struct wfs_timeval res_time;
		memset(&res_time, 0, sizeof(res_time));
		ret = os_sub_time_val(results->fetch_time, 0, res->ago,
				&res_time);
		if (ret)
			continue;
		if (os_is_time_before(&res_time, sta_info->scan_start_time)) {
			struct wfs_timeval ago;
			memset(&ago, 0, sizeof(ago));
			os_sub_time(sta_info->scan_start_time, &res_time, &ago);
			wfs_printf(LOG_DEBUG, "Ignore scan result that is "
					"%u.%06u seconds older than triggered "
					"scan", (unsigned int) ago.sec,
					(unsigned int) ago.usec);
			continue;
		}
		/** Finally this scan result can be kept */
		res->keep = 1;
	}
}


/***********************************************************
 *  Global functions
 ***********************************************************
 */
void sta_assoc_timer(void *ctx)
{
	struct wfs_iface *cur_if = ctx;
	enum wfs_internal_err ret = WFS_INTERNAL_ERR_SUCCESS;

	assert(ctx);

	ENTER();

	ret = sta_assoc(cur_if);
	if (ret) {
		/** TODO: error handling */
	}

	LEAVE();
}


enum wfs_internal_err
sta_cancel_scan(struct wfs_iface *cur_if)
{
	enum wfs_internal_err ret = WFS_INTERNAL_ERR_SUCCESS;

	assert(cur_if);

	ENTER();

	wfs_loop_unregister_timer(sta_scan_timer, cur_if);
	ret = wfs_driver_stop_sched_scan(cur_if);

	if (ret)
		wfs_printf(LOG_ERR, "Failed to cancel STA scan.");

	LEAVE();
	return ret;
}


void sta_deinit(struct wfs_sta_info **sta_info)
{
	ENTER();

	os_free_time(&(*sta_info)->scan_start_time);
	free_assoc_params((*sta_info)->assoc_params);
	free_scan_params((*sta_info)->scan_params);
	free_scan_results((*sta_info)->scan_results);
	FREE(*sta_info);

	LEAVE();
}


enum wfs_internal_err
sta_evt_scan_results(struct wfs_iface *cur_if, union wfs_event_data *event)
{
	enum wfs_internal_err ret = WFS_INTERNAL_ERR_SUCCESS;
	struct wfs_timeval diff;
	struct wfs_driver_scan_results *results = NULL;

	ENTER();

	assert(cur_if && cur_if->sta_info);

	memset(&diff, 0, sizeof(diff));

	/** Trigger the pending associate */
	if (cur_if->sta_info->status.associate == STA_ASSOC_PENDING) {
		cur_if->sta_info->status.associate = STA_ASSOC_NONE;
		wfs_loop_unregister_timer(sta_assoc_timer, cur_if);
		ret = wfs_loop_register_timer(0, 0, sta_assoc_timer, cur_if);
		if (ret)
			goto out;
	}

	ret = os_time_till_now(cur_if->sta_info->scan_start_time, &diff);
	if (ret)
		goto out;

	wfs_printf(LOG_DEBUG, "Scan completed in %ld.%06ld seconds",
			diff.sec, diff.usec);

	results = wfs_driver_get_scan_results(cur_if);
	if (!results) {
		ret = WFS_INTERNAL_ERR_NOENT;
		goto out;
	}

	if (!results->fetch_time) {
		ret = os_new_time(&results->fetch_time);
		if (ret)
			goto out;
	}
	if (!os_is_time_populated(results->fetch_time)) {
		ret = os_get_time(results->fetch_time);
		if (ret)
			goto out;
	}

	qsort(results->table, results->num,
		sizeof(struct wfs_driver_scan_result *),
		sta_scan_result_compar);
	print_scan_results(results);

	free_scan_results(cur_if->sta_info->scan_results);
	cur_if->sta_info->scan_results = results;

	validate_scan_results(cur_if);
out:
	LEAVE();
	return ret;
}


enum wfs_internal_err
sta_evt_trigger_scan(struct wfs_iface *cur_if)
{
	enum wfs_internal_err ret = WFS_INTERNAL_ERR_SUCCESS;

	ENTER();

	assert(cur_if && cur_if->sta_info);

	ret = os_get_time(cur_if->sta_info->scan_start_time);
	if (ret)
		goto out;

out:
	LEAVE();
	return ret;
}


enum wfs_internal_err
sta_init(struct wfs_sta_info **sta_info)
{
	enum wfs_internal_err ret = WFS_INTERNAL_ERR_SUCCESS;

	ENTER();

	*sta_info = (struct wfs_sta_info *) zalloc(sizeof(**sta_info));
	if (!*sta_info) {
		ret = WFS_INTERNAL_ERR_OOM;
		wfs_printf(LOG_ERR, "%s", err_to_str(ret));
		goto out;
	}

	ret = os_new_time(&(*sta_info)->scan_start_time);
	if (ret)
		goto out;

out:
	LEAVE();
	return ret;
}


void sta_scan_timer(void *ctx)
{
	struct wfs_iface *cur_if = ctx;
	enum wfs_internal_err ret = WFS_INTERNAL_ERR_SUCCESS;

	assert(ctx);

	ENTER();

	ret = sta_scan(cur_if);
	if (ret) {
		/** TODO: error handling */
	}

	LEAVE();
}
